package project3;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

import project2.HttpRequestFormatErrorException;

public class BrowserFacingThread extends Thread {
	private static int ERRORLIMIT = 10;
	private static int RELAY_BEGIN_ATTEMPTS = 3; // attempts to send a new relay begin before giving up
	
	private boolean dead;
	private Socket socket;
	private int errorLimit;
	private BufferedReader in;
	private DataOutputStream out;
	
	
	public BrowserFacingThread(Socket socket) {
		super();
		this.dead = false;
		this.socket = socket;
		this.errorLimit = ERRORLIMIT;
		
		try {
			out = new DataOutputStream(socket.getOutputStream());
		} catch (IOException e) {
			System.out.println("[BFThread] Error when initializing the BrowserFacing OutputStream at socket " + socket);			
			terminate();
		}
		
	    try {
			in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		} catch (IOException e) {
			System.out.println("[BFThread] Error when initializing the BrowserFacing InputStream at socket " + socket);
			terminate();
		}
	    
	    if (!this.isTerminate()) {
	    	System.out.println("[BFThread] New Browser Request from " + socket.getInetAddress().getHostAddress() + " Registered. ");
	    }	    
	}
	
	/**
	 * Listen the input information from web browser, choose circuit, create stream, send data, 
	 * listen input from other intermidiate router, send data back to browser.
	 */
	public void run() {
		
		while (!dead) {
			if (socket.isClosed() || errorLimit < 0) {
				break;
			}
			
			String inputLine = null;
			List<String> request = new ArrayList<String>();
			try {
				// loop read request lines
				while ((inputLine = in.readLine()) != null) {
					// Check the end of the request.
					if (inputLine.equals(""))
						break;
					request.add(inputLine);
				}
				
				if (request.isEmpty()) {
					errorLimit --;
					continue;
				}
			} catch (IOException e) {
				if (!dead) {
					System.out.println("[BFThread] Error when reading the inputline from user at socket " + socket);
					terminate();
					errorLimit --;
				}
			} catch (HttpRequestFormatErrorException e) {
				if (!dead) {
					e.printStackTrace();
					errorLimit --;
				}
			}
			
			// list of bytes to be sent
			List<Byte> toSend = new ArrayList<Byte>();
			String hostName = null;
			int port = 80;
			
			for (String s : request) {
				// split s into a kv pair (if not a GET or POST line)				
				String[] raw = s.split(": ", 2);
				
				if (!s.contains("GET ") && !s.contains("POST ") && raw.length < 2) {
					throw new HttpRequestFormatErrorException("Error Http Request Format for request " + request + ".");
				}
				
				byte [] sBytes;
				if (raw[0].equals("Connection") && raw[1].equals("keep-alive")) {
					// change keep-alive to close
					raw[1] = "close";
					String newString = raw[0] + ": " + raw[1];
					sBytes = newString.getBytes();
				} else {
					// parse the host name when we see one
					if (raw[0].equalsIgnoreCase("Host")) {
						String[] hostport = raw[1].split(":");
						hostName = hostport[0];
						if (hostport.length > 1) {
							// use the specified port number when we see one
							port = Integer.parseInt(hostport[1]);
						}
					}
					sBytes = s.getBytes();
				}
				
				// dump bytes into list
				for (int i = 0; i < sBytes.length; i++)
					toSend.add(sBytes[i]);
				
				toSend.add((byte) (13)); // add CR
				toSend.add((byte) (10)); // add LF
			}
			
			// add an empty line
			toSend.add((byte) (13)); // add CR
			toSend.add((byte) (10)); // add LF
			
			// get circuit and stream id
			int streamId = 0;
			int circId = 0;
			System.out.println("[BFThread] Start creating circuit. ");
			synchronized (this) {
				streamId = SourceRouter.generateNextStreamId();
				circId = SourceRouter.chooseCircuit(streamId);
				
				SourceRouter.registerBrowserFacingThread(streamId, this);
			}
			
			IntermediateRouter.registerBrowser(circId, socket);

			
			System.out.println("[BFThread] Finish creating circuit. ");
			
			/**************** start of stream creation ****************/
			if (hostName == null) 
				throw new HttpRequestFormatErrorException("Request contains no valid host name");
			
			String relayBeginBodyStr = hostName + ":" + port + "\0";
			byte [] relayBeginBody = relayBeginBodyStr.getBytes();
			
			// create a relay begin cell 
			RelayCell relayCell = new RelayCell(circId,
					streamId,
					relayBeginBody.length,
					(byte) 0x01, // RELAY BEGIN CMD
					relayBeginBody);
			
			boolean streamCreationIsSuccessful = false;
			
			for (int i = 0; i < RELAY_BEGIN_ATTEMPTS; i++) {
				// send the relay begin cell
				System.out.println("[BFThread] Begin to send RELAY BEGIN");
				SourceRouter.sendSourcePackage(relayCell, circId);
				
				// get the response cell
				Cell response = Router.getWantedCell(circId);
				
				// check if the response indicates success
				if (((RelayCell) response).getRelayCmdType() == (byte) 0x04) { // RELAY CONNECTED CMD
					streamCreationIsSuccessful = true;
					break;
				}
			}
			/**************** end of stream creation ****************/
			
			
			if (streamCreationIsSuccessful) { // prepare to send the entire request data
				// move the bytes to be sent into an array
				byte[] toSendArr = new byte[toSend.size()];
				for (int i = 0; i < toSend.size(); i++) {
					toSendArr[i] = toSend.get(i);
				}
				
				System.out.println("[BFThread] Begin to send data.");
				// divide byte array into cells
				// TODO: move the method to util
				List<RelayCell> cells = Util.cutByteArrayIntoCells(toSendArr, circId, streamId);
				
				SourceRouter.sendSourcePackage(cells, circId);
			} else {
				// send error page back to client
			}
		}		
	}
	
	
	public void terminate() {
		this.dead = true;
		try {
			socket.close();
			in.close();
			out.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	public boolean isTerminate() {
		return this.dead;
	}
	
	public void writeBackToBrowser(byte[] data) {
		try {
			out.write(data);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
